<?php
namespace app\common\library\payment\type;

use app\common\library\payment\PayMent;
use think\Exception;

class Wxpay extends PayMent{
    // 应用Id
    private $appId = 'wx52fc30516af65339';
    
    // 商户号
    private $mchId = '1526197961';
    
    // 商户支付密钥
    private $key= '853fea839642ff93739bd7103c183f19';
    
    // 签名类型
    private $signType= 'MD5';

    public function unifiedorder($title, $out_trade_no, $total)
    {
        $url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
        
        // 组合支付参数
        $params = [];
        $params['appid'] = $this->appId;
        $params['mch_id'] = $this->mchId;
        $params['nonce_str'] = $this->getNonceStr();
        $params['body'] = $title;
        $params['out_trade_no'] = $out_trade_no;
        $params['total_fee'] = $total * 100;
        $params['spbill_create_ip'] = request()->ip();
        $params['notify_url'] = url('api/pay/wx_notify', null, false, true);
        $params['trade_type'] = 'APP';
        $params['sign'] = $this->sign($params);
        
        $xml = $this->toXml($params);
        $response = $this->postXmlCurl($xml, $url);
        $result = $this->getResult($response);
        return $result;
    }

    public function check($data)
    {
        // 如果返回成功则验证签名
        try {
                // 失败则直接返回失败
            if (! array_key_exists('sign', $data)) {
                throw new Exception("签名错误！");
            }
            $sign = $this->sign($data);
            if ($data['sign'] == $sign) {
                // 签名正确
                return true;
            }
            throw new Exception("签名错误！");
            
        } catch (Exception $e){
            $msg = $e->errorMessage();
            return false;
        }
    }

    /**
     * 以post方式提交xml到对应的接口url
     *
     * @param string $xml  需要post的xml数据
     * @param string $url  url
     * @param bool $useCert 是否需要证书，默认不需要
     * @param int $second   url执行超时时间，默认30s
     * @throws Exception
     */
    private function postXmlCurl($xml, $url, $useCert = false, $second = 30)
    {
        $ch = curl_init();
        $curlVersion = curl_version();
        $ua = "WXPaySDK/3.0.9 (".PHP_OS.") PHP/".PHP_VERSION." CURL/".$curlVersion['version']." ".$this->mchId;
        
        //设置超时
        curl_setopt($ch, CURLOPT_TIMEOUT, $second);
            
        $proxyHost = "0.0.0.0";
        $proxyPort = 0;
        // 如果有配置代理这里就设置代理
        if ($proxyHost != "0.0.0.0" && $proxyPort != 0) {
            curl_setopt($ch, CURLOPT_PROXY, $proxyHost);
            curl_setopt($ch, CURLOPT_PROXYPORT, $proxyPort);
        }
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); // 严格校验
        curl_setopt($ch, CURLOPT_USERAGENT, $ua);
        // 设置header
        curl_setopt($ch, CURLOPT_HEADER, FALSE);
        // 要求结果为字符串且输出到屏幕上
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        
        if ($useCert == true) {
            // 设置证书
            // 使用证书：cert 与 key 分别属于两个.pem文件
            // 证书文件请放入服务器的非web目录下
            $sslCertPath = "";
            $sslKeyPath = "";
            curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
            curl_setopt($ch, CURLOPT_SSLCERT, $sslCertPath);
            curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
            curl_setopt($ch, CURLOPT_SSLKEY, $sslKeyPath);
        }
        // post提交方式
        curl_setopt($ch, CURLOPT_POST, TRUE);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
        // 运行curl
        $data = curl_exec($ch);
        // 返回结果
        if ($data) {
            curl_close($ch);
            return $data;
        } else {
            $error = curl_errno($ch);
            curl_close($ch);
            throw new Exception("curl出错，错误码:$error");
        }
    }
    
    /**
     * 产生随机字符串，不长于32位
     * @param int $length
     * @return string 产生的随机字符串
     */
    private function getNonceStr($length = 32)
    {
        $chars = "abcdefghijklmnopqrstuvwxyz0123456789";
        $str = "";
        for ($i = 0; $i < $length; $i ++) {
            $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
        }
        return $str;
    }
    
    /**
     * 生成签名
     * @param array $values 数据
     * @return string $result 
     */
    private function sign($values)
    {
        // 签名步骤一：按字典序排序参数
        ksort($values);
        $string = $this->toUrlParams($values);
        // 签名步骤二：在string后加入KEY
        $string = $string."&key=".$this->key;
        // 签名步骤三：MD5加密或者HMAC-SHA256
        if ($this->signType == "MD5") {
            $string = md5($string);
        } else if ($this->signType == "HMAC-SHA256") {
            $string = hash_hmac("sha256", $string, $this->key);
        } else {
            throw new Exception("签名类型不支持！");
        }
        
        // 签名步骤四：所有字符转为大写
        $result = strtoupper($string);
        return $result;
    }
    
    /**
     * 验签
     * @param array $values
     * @throws Exception
     * @return boolean
     */
    private function checkSign($values)
    {
        if (! isset($values['sign']) || empty($values['sign'])) {
            throw new Exception("签名错误！");
        }
        
        $sign = $this->sign($values);
        if ($values['sign'] == $sign) {
            // 签名正确
            return true;
        }
        throw new Exception("签名错误！");
    }
    
    /**
     * 输出xml字符
     * @param array $values
     * @throws Exception
     */
    private function toXml($values)
    {
        if (! is_array($values) || count($values) <= 0) {
            throw new Exception("数组数据异常！");
        }
        
        $xml = "<xml>";
        foreach ($values as $key => $val) {
            if (is_numeric($val)) {
                $xml .= "<".$key.">".$val."</".$key.">";
            } else {
                $xml .= "<".$key."><![CDATA[".$val."]]></".$key.">";
            }
        }
        $xml .= "</xml>";
        return $xml;
    }
    
    /**
     * 将xml转为array
     * @param string $xml
     * @throws Exception
     */
    private function fromXml($xml)
    {
        if(!$xml){
            throw new Exception("xml数据异常！");
        }
        //将XML转为array
        //禁止引用外部xml实体
        libxml_disable_entity_loader(true);
        return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
    }

    /**
     *
     * @param string $xml
     * @throws Exception
     * @return boolean|mixed
     */
    private function getResult($xml)
    {
        $values = $this->fromXml($xml);
        // 失败则直接返回失败
        if ($values['return_code'] != 'SUCCESS') {
            foreach ($values as $key => $value) {
                // 除了return_code和return_msg之外其他的参数存在，则报错
                if ($key != "return_code" && $key != "return_msg") {
                    throw new Exception("输入数据存在异常！");
                    return false;
                }
            }
        }
        $this->checkSign($values);
        
        // 在次签名
        $info = [];
        $info['appid'] = $this->appId;
        $info['partnerid'] = $this->mchId;
        $info['prepayid'] = $values['prepay_id'];
        $info['package'] = 'Sign=WXPay';
        $info['noncestr'] = $values['nonce_str'];
        $info['timestamp'] = time();
        $info['sign'] = $this->sign($info);
        
        return $info;
    }

    /**
     * 格式化参数格式化成url参数
     * 
     * @param array $values
     * @return string
     */
    private function toUrlParams($values)
    {
        $buff = "";
        foreach ($values as $k => $v) {
            if ($k != "sign" && $v != "" && ! is_array($v)) {
                $buff .= $k."=".$v."&";
            }
        }
        
        $buff = trim($buff, "&");
        return $buff;
    }
    
}